﻿#region
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using DeepEarth.Core;
using DeepEarth.Core.Controls;
using DeepEarth.Core.Data;
using DeepEarth.Core.Utilities;
using DeepEarth.Toolkit.Common;
using System.Threading;
#endregion

namespace DeepEarth.Toolkit.Controls
{
    [TemplatePart(Name = PART_ToolPanel, Type = typeof(FindControlToolPanel))]
    [TemplatePart(Name = PART_ItemSearchResults, Type = typeof(ItemsControl))]
    public class FindControl : MapInteractionControl
    {
        const string PART_ToolPanel = "PART_ToolPanel";
        const string PART_ItemSearchResults = "PART_ItemSearchResults";

        public FindControl()
        {
            DefaultStyleKey = typeof(FindControl);
            Initialize();

            ItemSelectedCommand = new LambdaCommand<object>(HandleSelection);
            ClearResultsCommand = new LambdaCommand(OnClearResults);
        }

        #region Properties
        public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(FindControl), null);
        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        public static readonly DependencyProperty TemplateSelectorProperty = DependencyProperty.Register("TemplateSelector", typeof(TemplateSelector), typeof(FindControl), null);
        public TemplateSelector TemplateSelector
        {
            get { return (TemplateSelector)GetValue(TemplateSelectorProperty); }
            set { SetValue(TemplateSelectorProperty, value); }
        }

        public static readonly DependencyProperty TriggerSearchProperty = DependencyProperty.Register("TriggerSearchCommand", typeof(ICommand), typeof(FindControl), null);
        public ICommand TriggerSearchCommand
        {
            get { return (ICommand)GetValue(TriggerSearchProperty); }
            set { SetValue(TriggerSearchProperty, value); }
        }

        // Performs additional actions - in addition to setting any appropriate GeoItemViewModel to Selected
        // which will occur anyway
        public static readonly DependencyProperty ResultSelectedCommandProperty = DependencyProperty.Register("ResultSelectedCommand", typeof(ICommand), typeof(FindControl), null);
        public ICommand ResultSelectedCommand
        {
            get { return (ICommand)GetValue(ResultSelectedCommandProperty); }
            set { SetValue(ResultSelectedCommandProperty, value); }
        }

        public static readonly DependencyProperty SearchTermProperty = DependencyProperty.Register("SearchTerm", typeof(string), typeof(FindControl), null);
        public string SearchTerm
        {
            get { return (string)GetValue(SearchTermProperty); }
            set { SetValue(SearchTermProperty, value); }
        }

        public static readonly DependencyProperty AdditionalSearchProvidersProperty = DependencyProperty.Register("AdditionalSearchProviders", typeof(IEnumerable<Tuple<ISearchable, bool>>), typeof(FindControl), null);
        public IEnumerable<Tuple<ISearchable, bool>> AdditionalSearchProviders
        {
            get { return (IEnumerable<Tuple<ISearchable, bool>>)GetValue(AdditionalSearchProvidersProperty); }
            set { SetValue(AdditionalSearchProvidersProperty, value); }
        }

        public static readonly DependencyProperty HeaderTemplateSelectorProperty = DependencyProperty.Register("HeaderTemplateSelector", typeof(TemplateSelector), typeof(FindControl), null);
        public TemplateSelector HeaderTemplateSelector
        {
            get { return (TemplateSelector)GetValue(HeaderTemplateSelectorProperty); }
            set { SetValue(HeaderTemplateSelectorProperty, value); }
        }

        public static readonly DependencyProperty ItemSelectedCommandProperty = DependencyProperty.Register("ItemSelectedCommand", typeof(ICommand), typeof(FindControl), null);
        public ICommand ItemSelectedCommand
        {
            get { return (ICommand)GetValue(ItemSelectedCommandProperty); }
            set { SetValue(ItemSelectedCommandProperty, value); }
        }

        public static readonly DependencyProperty SearchResultsSourceProperty = DependencyProperty.Register("SearchResultsSource", typeof(ObservableCollection<SearchResult>), typeof(FindControl), null);
        public ObservableCollection<SearchResult> SearchResultsSource
        {
            get { return (ObservableCollection<SearchResult>)GetValue(SearchResultsSourceProperty); }
            set { SetValue(SearchResultsSourceProperty, value); }
        }

        public string SearchMemberPath { get; set; }

        // Select the GeoItemViewModel if clicked in the result pane
        public bool SelectResult { get; set; }

        // Execute the GeoItemViewModel's Navigate Command if selected in the result pane
        public bool NavigateResult { get; set; }

        int outstandingSearches;

        public static readonly DependencyProperty IsSearchingProperty = DependencyProperty.Register("IsSearching", typeof(bool), typeof(FindControl), new PropertyMetadata(false));
        public bool IsSearching
        {
            get { return (bool)GetValue(IsSearchingProperty); }
            set { SetValue(IsSearchingProperty, value); }
        }

        Guid searchToken;
        #endregion

        void Initialize()
        {
            SearchResultsSource = new ObservableCollection<SearchResult>();
            TriggerSearchCommand = new LambdaCommand<string>(Search);
        }

        public void Search(string searchTerm)
        {
            IsSearching = false;
            outstandingSearches = 0;

            searchToken = Guid.NewGuid();

            if (!string.IsNullOrEmpty(searchTerm.Trim()))
            {
                SearchTerm = searchTerm;
                Search();
            }
        }

        void Search()
        {
            if (!string.IsNullOrWhiteSpace(SearchTerm))
            {
                OnSearchStarted();
                SearchResultsSource = new ObservableCollection<SearchResult>();

                int nextIndex = SearchAdditionalProviders(GeoItems.ViewModels.Count);

                var effectiveSearchStrategy = SearchStrategy;

                // If no SearchMemberPath has been set, then set the search strategy to top level and hope to encounter ISearchables
                if (effectiveSearchStrategy == SearchStrategy.InMemory && string.IsNullOrEmpty(SearchMemberPath))
                {
                    effectiveSearchStrategy = SearchStrategy.TopLevel;
                }

                // switch on search strategy!!
                switch (effectiveSearchStrategy)
                {
                    case SearchStrategy.InMemory :
                        InMemorySearch(nextIndex);
                        break;

                    case SearchStrategy.TopLevel :
                        TopLevelSearch(nextIndex);
                        break;

                    case SearchStrategy.Hierarchy :
                        HierarchySearch(nextIndex);
                        break;
                }
            }
        }

        int SearchAdditionalProviders(int resultIndex)
        {
            int retVal = 0;

            if (AdditionalSearchProviders != null)
            {
                foreach (var searchable in AdditionalSearchProviders)
                {
                    int searchResultIndex = resultIndex;

                    if (searchable.Item2)
                    {
                        searchResultIndex = retVal;
                        retVal++;
                    }

                    searchable.Item1.Search(SearchTerm, CreateResultHandler(searchable.Item1, searchResultIndex, searchToken));
                    resultIndex++;
                }
            }

            return retVal;
        }

        void HandleSelection(object item)
        {
            var selectedItem = item as ISearchable;

            if (selectedItem != null)
            {
                var completeHierarchy = selectedItem.Hierarchy;

                // Hierarchy can return null. In which case assume there isn't one and the structure is flat
                if (selectedItem.Hierarchy == null)
                {
                    completeHierarchy = new List<IComparable> { selectedItem.Id };
                }
                else if (!selectedItem.Hierarchy[selectedItem.Hierarchy.Count - 1].Equals(selectedItem.Id))
                {
                    // the hierarchy returned by the item should include itself. If not add it at the end
                    completeHierarchy = selectedItem.Hierarchy;
                    completeHierarchy.Add(selectedItem.Id);
                }

                // Find the view model associated with the selected item. Load it if hasn't been loaded.
                GetSearchItemViewModel(completeHierarchy);
            }
            else
            {
                GetLoadedItemViewModel(null);
            }

            // Preemptively invoke this
            if (ResultSelectedCommand != null)
            {
                ResultSelectedCommand.Execute(item);
            }
        }

        Action<GeoItemViewModel> GetSelectionCallback()
        {
            return item => Dispatcher.BeginInvoke(() =>
                {
                    if (SelectResult)
                        item.IsSelected = true;

                    if (NavigateResult)
                        item.Navigate();

                    if (ResultSelectedCommand != null)
                        ResultSelectedCommand.Execute(item.Data);
                });
        }

        void GetSearchItemViewModel(List<IComparable> totalHierarchy)
        {
            GeoItemViewModelHelper.FindSeachableItemViewModel(GeoItems.ViewModels, totalHierarchy, GetSelectionCallback());
        }

        void GetLoadedItemViewModel(object item)
        {
            GeoItemViewModelHelper.FindLoadedItemViewModel(GeoItems.ViewModels, item, GetSelectionCallback());
        }
      
        #region search strategies
        public static readonly DependencyProperty SearchStrategyProperty = DependencyProperty.Register("SearchStrategy", typeof(SearchStrategy), typeof(FindControl), new PropertyMetadata(SearchStrategy.InMemory));
        public SearchStrategy SearchStrategy
        {
            get { return (SearchStrategy)GetValue(SearchStrategyProperty); }
            set { SetValue(SearchStrategyProperty, value); }
        }

        Action<IEnumerable> CreateResultHandler(object source, int resultIndex, Guid requestToken)
        {
            IsSearching = true;
            Interlocked.Increment(ref outstandingSearches);

            Action<IEnumerable> callback = hits =>
                {
                    // Ensure search resquest is not obsolete
                    if (searchToken != requestToken)
                        return;

                    if (Interlocked.Decrement(ref outstandingSearches) == 0)
                        IsSearching = false;

                    // Error in call, moving on...
                    if (hits == null)
                        return;

                    var sr = new SearchResult
                    {
                        Source = source,
                        Results = new ObservableCollection<object>()
                    };

                    // add all of the hits to the results collection
                    foreach (var result in hits)
                        sr.Results.Add(result);

                    // if the incoming results are incomplete, add new results as they come in
                    var observableResults = hits as INotifyCollectionChanged;
                    if (observableResults != null)
                    {
                        observableResults.CollectionChanged += (o, e) =>
                        {
                            if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)
                            {
                                foreach (var newItem in e.NewItems)
                                    sr.Results.Add(newItem);
                            }
                        };
                    }

                    if (resultIndex <= 0)
                    {
                        SearchResultsSource.Insert(0, sr);
                    }
                    else if (SearchResultsSource.Count > resultIndex)
                        SearchResultsSource.Insert(resultIndex, sr);
                    else
                        SearchResultsSource.Add(sr);
                };

            return callback;
        }

        void TopLevelSearch(int nextIndex)
        {
            foreach (var viewModel in GeoItems.ViewModels)
            {
                var item = viewModel.Data as ISearchable;

                if (item != null)
                {
                    item.Search(SearchTerm, CreateResultHandler(item, nextIndex, searchToken));
                }
            }
        }

        void HierarchySearch(int nextIndex)
        {
            foreach (var viewModel in GeoItems.ViewModels)
            {
                InnerHierarchySearch(viewModel, nextIndex++);
            }
        }

        void InnerHierarchySearch(GeoItemViewModel viewModel, int nextIndex)
        {
            var item = viewModel.Data as ISearchable;

            if (item != null)
            {
                item.Search(SearchTerm, CreateResultHandler(item, nextIndex, searchToken));
            }

            if (viewModel.Children != null)
            {
                foreach (var child in viewModel.Children)
                {
                    InnerHierarchySearch(child, nextIndex);
                }
            }
        }

        void InMemorySearch(int nextIndex)
        {
            if (string.IsNullOrEmpty(SearchMemberPath))
            {
                return;
            }

            foreach (var viewModel in GeoItems.ViewModels)
            {
                var results = new List<object>();
                InnerInMemorySearch(results, viewModel);

                var callback = CreateResultHandler(viewModel.Data, nextIndex, searchToken);
                callback(results);
            }
        }

        void InnerInMemorySearch(List<object> results, GeoItemViewModel viewModel)
        {
            var val = GetMemberValue(viewModel.Data, SearchMemberPath);

            if (val != null)
            {
                if (val.ToString().ToLower().Contains(SearchTerm.ToLower()))
                {
                    results.Add(viewModel.Data);
                }
            }

            if (viewModel.Children != null)
            {
                foreach (var child in viewModel.Children)
                {
                    InnerInMemorySearch(results, child);
                }
            }
        }

        static object GetMemberValue(object obj, string propertyPath)
        {
            var t = obj.GetType();
            var pi = t.GetProperty(propertyPath);
            return pi.GetValue(obj, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, null, null, null);
        }
        #endregion

        #region Commands
        public static readonly DependencyProperty ClearResultsCommandProperty = DependencyProperty.Register("ClearResultsCommand", typeof(ICommand), typeof(FindControl), null);
        public ICommand ClearResultsCommand
        {
            get { return (ICommand)GetValue(ClearResultsCommandProperty); }
            set { SetValue(ClearResultsCommandProperty, value); }
        }

        void OnClearResults()
        {
            SearchResultsSource = null;
        }
        #endregion

        #region Events
        public event EventHandler SearchStarted;
        protected virtual void OnSearchStarted()
        {
            if (SearchStarted != null)
                SearchStarted(this, EventArgs.Empty);
        }
        #endregion
    }
}